package station;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class CommunicationModule {

	private InetAddress group;
	private int dsoPort;
	private int dsiPort;
	private int multicastPort;
	private byte nextSlot;
	private byte[] buffer;
	private byte[] dsBuffer;
	private String intfAddr;
	private NetworkInterface netiface;
	private List<Byte> slotList;
	
	private List<Integer> offsets;
	private static int offset = 0; 
	
	private static final int FRAMETIME			= 1000;
	private static final int SLOTS 				= 20;
	private static final int SLOTTIME 			= FRAMETIME / SLOTS;
	private static final int SLOTOFFSET 		= SLOTTIME / 2;
	private static final int SINKBUFFERSIZE 	= 41;
	private static final int DATABUFFERSIZE 	= 24;
	private static final int MESSAGEBUFFERSIZE 	= 33;
	private static final String HOST 			= "localhost"; 
	
	public CommunicationModule(int dsoPort, int dsiPort, String multicastAddress, int multicastPort, String intf) {
		this.multicastPort	= multicastPort;
		this.dsoPort		= dsoPort;
		this.dsiPort 		= dsiPort;
		this.buffer 		= new byte[MESSAGEBUFFERSIZE];
		this.dsBuffer 		= new byte[DATABUFFERSIZE];
		this.slotList 		= new ArrayList<Byte>(SLOTS);
		
		for (int i = 0; i < SLOTS; i++) {
			this.slotList.add((byte)i);
		}
		
		try {	
			this.intfAddr = null;
			for (InetAddress inetAddr : Collections.list(NetworkInterface.getByName(intf).getInetAddresses())) {
					if (inetAddr instanceof Inet4Address) {
						this.intfAddr = inetAddr.getHostAddress();
						break;
					}
			}
			this.netiface = NetworkInterface.getByInetAddress(InetAddress.getByName(this.intfAddr));
			this.group = InetAddress.getByName(multicastAddress);
		} catch (SocketException e) {
            e.printStackTrace();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		
		try {
			new DataSourceReceiver();
			new Sender();
			new Receiver();
		} catch (SocketException e) {
			e.printStackTrace();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		
		this.offsets = new LinkedList<Integer>();
		
	}
	
	public static long currentTimeMillis () {
		// use offset
		return System.currentTimeMillis() + offset;
		
		// don't use offset
//		return System.currentTimeMillis();
	}
	
	/**
	 * 
	 * Class Sender
	 * 
	 */
	private class Sender implements Runnable {

		private MulticastSocket socket;
		private boolean running;
		private DatagramPacket packet;
		private int noSlot;
		
		public Sender() {
			
			InetSocketAddress addr;
			try {
				addr		= new InetSocketAddress(InetAddress.getByName(CommunicationModule.this.intfAddr),
													0);
				this.socket = new MulticastSocket(addr);
			} catch (UnknownHostException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}	
			
			this.packet		= new DatagramPacket(	CommunicationModule.this.buffer, 
													CommunicationModule.this.buffer.length, 
													CommunicationModule.this.group, 
													CommunicationModule.this.multicastPort);
			this.noSlot		= 0;
			this.running	= true;
			Thread thread	= new Thread(this);
			thread.start();
		}
		
		@Override
		public void run() {

			// is there a free slot?
			long time = CommunicationModule.currentTimeMillis();
			long tmpOffset = FRAMETIME - (time % FRAMETIME) + 980;

			try {
				// sleep till the last station was sending
				Thread.sleep(tmpOffset);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}			
			
			if (CommunicationModule.this.slotList.size() > 0) {
				// found a free slot
				int slotPos 						= (int) (Math.random() * CommunicationModule.this.slotList.size());
				CommunicationModule.this.nextSlot	= CommunicationModule.this.slotList.get(slotPos);
			} else {
				// no free slot
				this.running = false;
			}
			
			while (this.running) {
								
				// syncronize to frame
				time = CommunicationModule.currentTimeMillis();
				tmpOffset = FRAMETIME - (time % FRAMETIME);

				try {
					Thread.sleep(tmpOffset);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}			
				
				// syncronize to slot
				tmpOffset = (SLOTTIME * CommunicationModule.this.nextSlot) + SLOTOFFSET;
				
				try {
					Thread.sleep(tmpOffset);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}			

				// build message
				ByteBuffer buf = ByteBuffer.wrap(CommunicationModule.this.buffer);
				buf.put(CommunicationModule.this.dsBuffer, 
						0, 
						CommunicationModule.this.dsBuffer.length);

				// determine next slot
				if (CommunicationModule.this.slotList.size() > 0) {
					int slotPos 						= (int) (Math.random() * CommunicationModule.this.slotList.size());
					CommunicationModule.this.nextSlot	= CommunicationModule.this.slotList.get(slotPos);
					this.noSlot 						= 0;
				} else {
//					System.out.println("no slot for you " + new String(CommunicationModule.this.dsBuffer) + " - " + CommunicationModule.this.slotList.size());
					if (++this.noSlot > 1)
						this.socket.close();
				}
				
				buf.put(24, 
						CommunicationModule.this.nextSlot);
				buf.putLong(25, 
							CommunicationModule.currentTimeMillis());
				this.packet.setData(CommunicationModule.this.buffer);
				
				try {
					this.socket.send(this.packet);
				} catch (IOException e) {
					// Failure, close all
					this.running = false;
					this.socket.close();
				}
				
			}
			
		}
		
	}

	/**
	 * 
	 * Class DataSourceReceiver
	 *
	 */
	private class DataSourceReceiver implements Runnable {
		
		private DatagramSocket socket;
		private DatagramPacket packet;
		private boolean running;
		
		public DataSourceReceiver() throws SocketException {
			this.packet		= new DatagramPacket(	CommunicationModule.this.dsBuffer, 
													DATABUFFERSIZE);
			this.socket		= new DatagramSocket(CommunicationModule.this.dsoPort);
			this.running	= true;
			Thread thread	= new Thread(this);
			thread.start();
		}
		
		@Override
		public void run() {
			while (this.running) {
				try {
					// receive package from DataSource
					this.socket.receive(this.packet);
				} catch (IOException e) {
					// Failure, close all
					this.running = false;
					this.socket.close();
					e.printStackTrace();
				}
			}
		}
		
	}
	
	/**
	 * 
	 * Class Receiver
	 *
	 */
	private class Receiver implements Runnable {

		private boolean running;
		private MulticastSocket socket;
		private DatagramSocket dsiSocket;
		private DatagramPacket dsiPacket;
		private byte[] buffer;
		private byte[] dsiBuffer;
		
		public Receiver() throws SocketException, UnknownHostException {
			
			 InetSocketAddress addr = new InetSocketAddress(	CommunicationModule.this.group,
					 											CommunicationModule.this.multicastPort);
			 
             try {
				this.socket = new MulticastSocket(CommunicationModule.this.multicastPort);
				this.socket.joinGroup(addr, CommunicationModule.this.netiface);
			} catch (IOException e) {
				this.running = false;
				e.printStackTrace();
			}
			
			this.buffer 	= new byte[MESSAGEBUFFERSIZE];
			this.dsiBuffer	= new byte[SINKBUFFERSIZE];
			this.dsiPacket	= new DatagramPacket(	this.dsiBuffer, 
													SINKBUFFERSIZE, 
													InetAddress.getByName(HOST), 
													CommunicationModule.this.dsiPort);
			this.dsiSocket	= new DatagramSocket();
			this.running	= true;
			Thread thread	= new Thread(this);
			thread.start();
		}
		
		@Override
		public void run() {
			DatagramPacket packet = new DatagramPacket(	this.buffer, 
														MESSAGEBUFFERSIZE);
			byte[] tmpMsg = new byte[DATABUFFERSIZE];
			while (this.running) {
				try {
					this.socket.setSoTimeout((int)(FRAMETIME - (CommunicationModule.currentTimeMillis() % FRAMETIME)));
					
					// receive packet
					this.socket.receive(packet);
					
					// save receive time
					long timestamp	= CommunicationModule.currentTimeMillis();
					
					// handle data		
					ByteBuffer buf	= ByteBuffer.wrap(packet.getData());
					buf.get(tmpMsg, 
							0, 
							tmpMsg.length);
					byte tmpSlot	= buf.get(24);
					long tmpTime	= buf.getLong(25);
					
					// send message to DataSink
					buf = ByteBuffer.wrap(this.dsiBuffer);
					buf.put(tmpMsg, 
							0, 
							tmpMsg.length);
					buf.put(24, 
							tmpSlot);
					buf.putLong(25, 
								tmpTime);
					buf.putLong(33, 
								timestamp);
					this.dsiSocket.send(this.dsiPacket);
					
					// update available slots		
					int index = CommunicationModule.this.slotList.indexOf(tmpSlot);
					if (index != -1) {
						CommunicationModule.this.slotList.remove(index);			
					}
					
					// save current offset
					if (CommunicationModule.this.offsets.size() >= 20) {
						CommunicationModule.this.offsets.remove(0);
					}
					CommunicationModule.this.offsets.add((int) (tmpTime - timestamp));
					
				} catch (SocketTimeoutException e) {
					CommunicationModule.this.slotList.clear();
					for (int i = 0; i < SLOTS; i++) {
						CommunicationModule.this.slotList.add((byte)i);
					}
					
					// update the global offest
					int tmp = 0;
					for (int i : CommunicationModule.this.offsets) {
						tmp += i;
					}
					if (CommunicationModule.this.offsets.size() > 0) {
						CommunicationModule.offset = tmp / CommunicationModule.this.offsets.size();						
					}
				} catch (IOException e) {
					// Failure, close all
					this.running = false;
					this.socket.close();
					this.dsiSocket.close();
					e.printStackTrace();
				}
				
			}
		}
		
	}
	
}
